#include <stdlib.h>

#include "evm_config.h"
#include "evm_error.h"
#include "evm_memory.h"


uint32_t evm_mem_initialize ( evm_memory **mem ) {
    uint32_t res = 0;
    if ( !mem ) {
        return ERR_NULLPTR;
    }

    // let's add a memory to the VM!
    *mem = malloc( sizeof( evm_memory ));

    if ( ! *mem ) {
        return ERR_MEMALLOC_FAILED;
    }

    (*mem)->allocated       = 0;
    (*mem)->free            = 0;
    (*mem)->chunks          = 0;
    (*mem)->index.allocated = 0;
    (*mem)->index.count     = 0;
    (*mem)->index.ptrs      = 0;
    (*mem)->index.types     = 0;

    res = evm_mem_add_chunk( *mem );
    if ( res != EVM_OK ) {
        free( *mem );
        *mem = 0;
    }

    return res;
}



uint32_t evm_mem_add_chunk  ( evm_memory *mem ) {
    evm_memchunk *chunk    = 0,
                 *existing = 0;
    uint32_t      i        = 0;

    // Creating memory chunk for VM
    chunk = malloc( sizeof( evm_memchunk ) );
    if ( !chunk ) {
        return ERR_MEMALLOC_FAILED;
    }

    chunk->data = 0;
    chunk->desc.exec  = 1;
    chunk->desc.read  = 1;
    chunk->desc.write = 1;
    chunk->size = EVM_BASE_MEMORY_CHUNK_SIZE;
    chunk->next = 0;

    chunk->data = malloc( EVM_BASE_MEMORY_CHUNK_SIZE );
    if (! chunk->data ) {
        free( chunk );
        return ERR_MEMALLOC_FAILED;
    }

    // Init data with zeroes;
    for ( i = 0; i < EVM_BASE_MEMORY_CHUNK_SIZE; i += 1 ) {
        chunk->data[i] = i;
    }

    // Appending chunk to others
    if ( mem->chunks ) {
        existing = mem->chunks;
        while ( existing->next != 0 ) {
            existing = existing->next;
        }
        existing->next = chunk;
    } else {
        mem->chunks = chunk;
    }

    // Making an index for this memchunk
    if ( mem->index.count > 0 ) {
        if ( mem->index.allocated > mem->index.count ) {
            // if there is a place in index for a new chunk
            mem->index.ptrs [ mem->index.count ] = chunk->data;
            mem->index.types[ mem->index.count ] = chunk->desc;
            mem->index.count += 1;
        } else {
            // if there is no place in index for a new chunk
            uint8_t      **index_ptrs  = 0;
            evm_mem_desc  *index_types = 0;
            uint32_t       i           = 0;

            // create new double-size index space
            index_ptrs  = (uint8_t**) malloc( sizeof(uint8_t*) * mem->index.allocated * 2 );
            index_types = (evm_mem_desc*) malloc( sizeof(evm_mem_desc) * mem->index.allocated * 2 );

            if ( !index_ptrs || !index_types ) {
                // if error while allocationg memory

                // remove just created chunk form chunk list
                existing = mem->chunks;
                if ( existing != chunk ) {
                    while ( existing->next != chunk ) {
                        existing = existing->next;
                    }
                    existing->next = 0;
                } else {
                    mem->chunks = 0;
                }

                // release created arrays for new index
                if ( index_ptrs ) {
                    free( index_ptrs );
                }
                if ( index_types ) {
                    free( index_types );
                }

                free( chunk->data );
                free( chunk );

                return ERR_MEMALLOC_FAILED;
            }

            mem->index.allocated = mem->index.allocated * 2;

            for ( i = 0; i < mem->index.count; i += 1 ) {
                // copy index data to the new arrays
                index_ptrs [ i ] = mem->index.ptrs [ i ];
                index_types[ i ] = mem->index.types[ i ];
            }

            index_ptrs [ i ] = chunk->data;
            index_types[ i ] = chunk->desc;

            i += 1;

            for ( ; i < mem->index.allocated * 2; i += 1 ) {
                // init othre elements with zeroes
                index_ptrs  [ i ] = 0;
                index_types [ i ].exec     = 0;
                index_types [ i ].read     = 0;
                index_types [ i ].write    = 0;
                index_types [ i ].reserved = 0;
            }

            // release old index arrays
            free( mem->index.ptrs  );
            free( mem->index.types );

            mem->index.ptrs  = index_ptrs;
            mem->index.types = index_types;

            // return is in the end of the function
        }
    } else {
        mem->index.ptrs  = malloc( sizeof(uint8_t*) * 8 ); //
        mem->index.types = malloc( sizeof(evm_mem_desc) * 8 );

        // test if allocation was successful
        if ( !mem->index.ptrs || !mem->index.types ) {
            existing = mem->chunks;
            if ( existing != chunk ) {
                while ( existing->next != chunk ) {
                    existing = existing->next;
                }
                existing->next = 0;
            } else {
                mem->chunks = 0;
            }

            if ( mem->index.ptrs ) {
                free( mem->index.ptrs );
            }

            if ( mem->index.types ) {
                free( mem->index.types );
            }

            free( chunk->data );
            free( chunk );

            return ERR_MEMALLOC_FAILED;
        }

        mem->index.allocated = 8;
        mem->index.count     = 1;

        mem->index.ptrs [ 0 ] = chunk->data;
        mem->index.types[ 0 ] = chunk->desc;
    } // end of making an index for the memchunk

    mem->allocated += EVM_BASE_MEMORY_CHUNK_SIZE;
    mem->free      += EVM_BASE_MEMORY_CHUNK_SIZE;

    printf("Allocated chunk of %d Kbytes\nTotal memory allocted: %d Kbytes\n", EVM_BASE_MEMORY_CHUNK_SIZE / 1024, mem->allocated / 1024);
    return EVM_OK;
}



uint32_t evm_mem_optimize   ( evm_memory *mem, uint8_t *wasSomethingOptimized ) {
    return ERR_NOT_SUPPORTED;
}



uint32_t evm_mem_total_size ( evm_memory *mem ) {
    return mem->allocated;
}



uint32_t evm_mem_total_used ( evm_memory *mem ) {
    return mem->used;
}



uint32_t evm_mem_total_free ( evm_memory *mem ) {
    return mem->free;
}


uint32_t evm_mem_uint8_at   ( evm_memory *mem, uint32_t ptr, uint8_t  *data ) {
    uint8_t  index  = ( ptr >> 23 ) & 0x1FF;
    uint32_t offset = ( ptr & 0x7FFFFF );

    if ( index >= mem->index.count ) {
        return ERR_MEM_BOUND;
    }

    *data = mem->index.ptrs[index][offset];
    return EVM_OK;
}


uint32_t evm_mem_uint32_at  ( evm_memory *mem, uint32_t ptr, uint32_t *data ) {
    uint8_t  index  = ( ptr >> 23 ) & 0x1FF;
    uint32_t offset = ( ptr & 0x7FFFFF );

    if ( index >= mem->index.count ) {
        return ERR_MEM_BOUND;
    }

    *data = *((uint32_t*)( mem->index.ptrs[index] + offset ));

    return EVM_OK;
}




uint32_t evm_mem_alloc      ( evm_memory *mem, uint32_t size ) {
    return ERR_NOT_SUPPORTED;
}

uint32_t evm_mem_free       ( evm_memory *mem, void     *ptr  ) {
    return ERR_NOT_SUPPORTED;
}



uint32_t evm_mem_write( evm_memory *mem, uint32_t addr, uint8_t *ptr, uint32_t size ) {
    uint8_t  index  = ( addr >> 23 ) & 0x1FF;
    uint32_t offset = ( addr & 0x7FFFFF );

    if ( !mem || !ptr ) {
        return ERR_NULLPTR;
    }

    while ( size > 0 ) {
        index  = ( addr >> 23 ) & 0x1FF;
        offset = ( addr & 0x7FFFFF );

        if ( index >= mem->index.count ) {
            evm_mem_add_chunk( mem );
        }

        mem->index.ptrs[index][offset] = *ptr;

        ptr  += 1;
        size -= 1;
        addr += 1;
    }


    return EVM_OK;
}



